package org.scs.carleton.ca.pgdt.core.ui.internal.highlighting;

import java.util.ArrayList;
import java.util.List;

import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.core.runtime.IStatus;
import org.eclipse.core.runtime.Status;
import org.eclipse.core.runtime.jobs.Job;
import org.eclipse.jface.text.IDocument;
import org.eclipse.jface.text.ITextInputListener;
import org.eclipse.jface.text.TextAttribute;
import org.eclipse.jface.text.TextPresentation;
import org.eclipse.jface.text.reconciler.MonoReconciler;
import org.eclipse.jface.text.source.ISourceViewer;
import org.eclipse.swt.widgets.Display;
import org.eclipse.swt.widgets.Shell;
import org.eclipse.ui.IWorkbenchPartSite;
import org.scs.carleton.ca.pgdt.core.ParseException;
import org.scs.carleton.ca.pgdt.core.PrologCore;
import org.scs.carleton.ca.pgdt.core.parser.nodes.IASTArgument;
import org.scs.carleton.ca.pgdt.core.parser.nodes.IASTClause;
import org.scs.carleton.ca.pgdt.core.parser.nodes.IASTCut;
import org.scs.carleton.ca.pgdt.core.parser.nodes.IASTExpression;
import org.scs.carleton.ca.pgdt.core.parser.nodes.IASTFunctor;
import org.scs.carleton.ca.pgdt.core.parser.nodes.IASTList;
import org.scs.carleton.ca.pgdt.core.parser.nodes.IASTLiteral;
import org.scs.carleton.ca.pgdt.core.parser.nodes.IASTNode;
import org.scs.carleton.ca.pgdt.core.parser.nodes.IASTNumber;
import org.scs.carleton.ca.pgdt.core.parser.nodes.IASTProgram;
import org.scs.carleton.ca.pgdt.core.parser.nodes.IASTStructure;
import org.scs.carleton.ca.pgdt.core.parser.nodes.IASTTranslationUnit;
import org.scs.carleton.ca.pgdt.core.parser.nodes.IASTVariable;
import org.scs.carleton.ca.pgdt.core.ui.editors.PrologEditor;
import org.scs.carleton.ca.pgdt.core.ui.internal.highlighting.PrologHighlightingManager.HighlightedPosition;
import org.scs.carleton.ca.pgdt.core.ui.internal.highlighting.PrologHighlightingManager.HighlightingStyle;
import org.scs.carleton.ca.pgdt.core.visitor.PrologVisitor;

/**
 * Enforces highlighting reconciliation
 * 
 * This will collect all of the positions to highlight 
 * 
 * @author tessierj
 *
 */
public class PrologHighlightingReconciler extends MonoReconciler implements ITextInputListener, IPrologReconcilingListener {
	private static final String PROLOG_HIGHLIGHTING = "Prolog Highlighting"; //$NON-NLS-1$
	
	/** Lock for reconciling */
	private Object reconcilationLock = new Object();
	/** Boolean flag for reconciliation */
	private boolean isReconciling = false;
	/** Lock for the job */
	private Object jobLock = new Object();
	/** The background job */
	private Job job;
	/** Highlighting presenter that this reconciler is owned by */
	private PrologHighlightingPresenter presenter;
	
	private List<HighlightedPosition> addedPositions;
	private List<HighlightedPosition> removedPositions;
	
	private PositionCollector collector;
	private ColorManager colorManager;
	
	private int numRemovedPositions;
	
	public PrologHighlightingReconciler(ColorManager colorManager) {
		super(new PrologHighlightingReconcilingStrategy(), true);
		addedPositions = new ArrayList<HighlightedPosition>(10);
		removedPositions = new ArrayList<HighlightedPosition>(10);
		collector = new PositionCollector();
		this.colorManager = colorManager;
	}
	
	/**
	 * The prolog editor
	 */
	private PrologEditor editor;
	
	@Override
	public void inputDocumentAboutToBeChanged(IDocument oldInput, IDocument newInput) {
		//When the document is about to be changed, we don't need to do anything until it has finished changing
		synchronized (jobLock) {
			if (job != null) {
				job.cancel();
				job = null;
			}
		}
	}

	@Override
	public void inputDocumentChanged(IDocument oldInput, IDocument newInput) {
		//Now that it has completed, we can start things up again
		if (newInput != null) {
			scheduleJob();
		}
	}
	
	public void install(PrologEditor editor, ISourceViewer sourceViewer, PrologHighlightingPresenter presenter) {
		this.editor = editor;
		this.presenter = presenter;
		
		if (editor != null) {
			editor.addReconcileListener(this);
		}
		else {
			sourceViewer.addTextInputListener(this);
			scheduleJob();
		}
	}

	public void reconciled(IASTTranslationUnit compilationUnit, boolean forced, IProgressMonitor monitor) {
		//Only one thread should reconcile at a time... and only once.  (They should not queue up, hence the blank return)
		synchronized (reconcilationLock) {
			if (isReconciling) {
				return;
			}
			else {
				isReconciling = true;
			}
		}
		
		IASTProgram program;
		try {
			program = PrologCore.getDefault().getASTProvider().getProgram(compilationUnit);
		}
		catch (ParseException e) {
			program = null;
		}
		try {
			//Now that we know we are not already reconciling
			if (program == null || monitor.isCanceled()) {
				return;
			}
			
			//Reset the cancel value, we're going in
			presenter.setCanceled(false);
			
			IASTNode[] subTrees = getAffectedSubtrees(program);
			if (subTrees.length > 0) {
				//Keep checking to make sure that this wasn't cancelled
				if (!monitor.isCanceled()) {
					startReconcilingPositions();
				}
				
				if (!monitor.isCanceled()) {
					reconcilePositions(subTrees);
				}
				
				TextPresentation textPresentation = null;
				if (!monitor.isCanceled()) {
					textPresentation = presenter.createPresentation(addedPositions, removedPositions);
				}
				
				if (!monitor.isCanceled()) {
					updatePresentation(textPresentation, addedPositions, removedPositions);
				}
			}
		}
		finally {
			synchronized (reconcilationLock) {
				isReconciling = false;
			}
		}
	}
	
	private IASTNode[] getAffectedSubtrees(IASTProgram program) {
		return new IASTNode[] {program};
	}
	
	private void startReconcilingPositions() {
		presenter.addAllPositions(removedPositions);
		numRemovedPositions = removedPositions.size();
	}

	private void reconcilePositions(IASTNode[] subTrees) {
		int subtreeLength = subTrees.length;
		addedPositions.clear();
		for (int i=0; i<subtreeLength; i++) {
			subTrees[i].accept(collector);
		}
		//TODO: Fix this logic
		List<HighlightedPosition> oldPositions = removedPositions;
		List<HighlightedPosition> newPositions = new ArrayList<HighlightedPosition>(numRemovedPositions);
		HighlightedPosition current;
		for (int i=0; i<numRemovedPositions; i++) {
			current = oldPositions.get(i); //This may be null
			if (current != null) {
				newPositions.add(current);
			}
		}
		removedPositions = newPositions;
	}

	private void updatePresentation(TextPresentation textPresentation, List<HighlightedPosition> addedPositions2, List<HighlightedPosition> removedPositions2) {
		Runnable runnable = presenter.createUpdateRunnable(textPresentation, addedPositions, removedPositions);
		if (runnable == null)
			return;

		if (editor == null)
			return;

		IWorkbenchPartSite site = editor.getSite();
		if (site == null)
			return;

		Shell shell = site.getShell();
		if (shell == null || shell.isDisposed())
			return;

		Display display = shell.getDisplay();
		if (display == null || display.isDisposed())
			return;

		display.asyncExec(runnable);
	}

	private void scheduleJob() {
		if (editor != null) {
			//Use the full ast for now.. move to a shell later on?
			final IASTProgram root = editor.getPrologProgram();
			
			synchronized (jobLock) {
				final Job oldJob = job;
				if (job != null) {
					job.cancel();
					job = null;
				}
				
				if (root != null) {
					job = new Job(PROLOG_HIGHLIGHTING) {
						protected IStatus run(IProgressMonitor monitor) {
							if (oldJob != null) {
								try {
									oldJob.join();
								}
								catch (InterruptedException e) {
									return Status.CANCEL_STATUS;
								}
								if (monitor.isCanceled()) {
									return Status.CANCEL_STATUS;
								}
								
								reconciled(root.getTranslationUnit(), false, monitor);
								
								synchronized (jobLock) {
									if (job == this)
										job = null;
								}
							}
							return Status.OK_STATUS;
						}
					};
					
					job.setSystem(true);
					job.setPriority(Job.DECORATE);
					job.schedule();
				}
			}
		}
	}

	private class PositionCollector implements PrologVisitor {
		private void addPosition(int offset, int length, HighlightingStyle style) {
			boolean exists = false;
			int size = removedPositions.size();
			HighlightedPosition position;
			for (int i=0; i<size; i++) {
				position = removedPositions.get(i);
				if (position != null && position.isEqual(offset, length, style)) {
					exists = true;
					removedPositions.set(i, null);
					--numRemovedPositions;
					break;
				}
			}
			
			if (!exists) {
				position = presenter.createHighlightedPosition(offset, length, style);
				addedPositions.add(position);
			}
		}
		
		@Override
		public boolean visit(IASTFunctor functor) {
			int offset = functor.getStartOffset();
			int length = functor.getLength();
			
			if (offset > -1 && length > 0) {
				IASTNode owner = functor.getOwner();
				if (owner instanceof IASTArgument) {
					addPosition(offset, length, new HighlightingStyle(new TextAttribute(colorManager.getColor(IPrologColorConstants.ATOM)), true));
				}
				else {
					addPosition(offset, length, new HighlightingStyle(new TextAttribute(colorManager.getColor(IPrologColorConstants.FUNCTOR)), true));
				}
			}
			return true;
		}

		@Override
		public boolean visit(IASTNumber number) {
			int offset = number.getStartOffset();
			int length = number.getLength();
			if (offset > -1 && length > 0) {
				addPosition(offset, length, new HighlightingStyle(new TextAttribute(colorManager.getColor(IPrologColorConstants.NUMBER_LITERAL)), true));
			}
			return true;
		}

		@Override
		public boolean visit(IASTVariable variable) {
			int offset = variable.getStartOffset();
			int length = variable.getLength();
			if (offset > -1 && length > 0) {
				addPosition(offset, length, new HighlightingStyle(new TextAttribute(colorManager.getColor(IPrologColorConstants.VARIABLE)), true));
			}
			return true;
		}

		@Override
		public boolean visit(IASTArgument argument) {
			return true;
		}

		@Override
		public boolean visit(IASTClause clause) {
			return true;
		}

		@Override
		public boolean visit(IASTList list) {
			return true;
		}

		@Override
		public boolean visit(IASTProgram program) {
			return true;
		}

		@Override
		public boolean visit(IASTStructure structure) {
			return true;
		}

		@Override
		public boolean visit(IASTTranslationUnit translationUnit) {
			return true;
		}

		@Override
		public boolean visit(IASTLiteral literal) {
			int offset = literal.getStartOffset();
			int length = literal.getLength();
			if (offset > -1 && length > 0) {
				addPosition(offset, length, new HighlightingStyle(new TextAttribute(colorManager.getColor(IPrologColorConstants.LITERAL)), true));
			}
			return true;
		}

		@Override
		public boolean visit(IASTCut cut) {
			return true;
		}

		@Override
		public boolean visit(IASTExpression expression) {
			return true;
		}
	}

	@Override
	public void preReconciliation() {
		//Do nothing
	}
}
